﻿//-----------------------------------------------------------------------
// <copyright file="ManipulatorToolBase.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Manipulator
{
    using System;
    using System.Collections.Generic;
    using TileTool.Base.Editor.Tool.Scene;
    using TileTool.Common.Math;
    using TileTool.Editor.Tool.Scene;
    using TileTool.Editor.Tool.Transform;
    using TileTool.Editor.Transform;
    using TileTool.Input;
    using TileTool.Render;
    using TileTool.Scene.Entity;
    using TileTool.Tool;

    /// <summary>
    /// Base class for manipulator classes.
    /// </summary>
    public abstract class ManipulatorToolBase : SceneToolBase, IManipulatorTool
    {
        #region Fields - Private

        /// <summary>
        /// List of entities that we are manipulating.
        /// </summary>
        private IList<ManipulatorEntity> manipulateEntities = new List<ManipulatorEntity>();

        #endregion

        #region Fields - Private

        /// <summary>
        /// Translate manipulator.
        /// </summary>
        private ManipulatorBase manipulator = null;

        #endregion

        #region Constructor

        /// <summary>
        /// Base manipulator constructor.
        /// </summary>
        /// <param name="manipulator">Manipulator that this tool will be using.</param>
        public ManipulatorToolBase(ManipulatorBase manipulator)
        {
            if (manipulator == null)
            {
                throw new ArgumentNullException("manipulator", "Manipulator must be non-null.");
            }

            this.manipulator = manipulator;
            this.manipulator.Owner = this;

            this.manipulator.Begin += this.ManipulateBegin;
            this.manipulator.End += this.ManipulateEnd;
            this.manipulator.Update += this.ManipulateUpdate;
            this.manipulator.Abort += this.ManipulateAbort;
        }

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets the current transform state.
        /// </summary>
        public ITransformState State
        {
            get;
            set;
        }

        #endregion

        #region Properties - Protected

        /// <summary>
        /// Gets if we are currently manipulating objects.
        /// </summary>
        protected bool Manipulating
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the manipulator this tool is using.
        /// </summary>
        protected ManipulatorBase Manipulator
        {
            get
            {
                return this.manipulator;
            }
        }

        /// <summary>
        /// Gets an enumerable of the entities we are currently manipulating.
        /// 
        /// Only valid to call this function when Manipulating is true.
        /// </summary>
        protected IEnumerable<ManipulatorEntity> Entities
        {
            get
            {
                return this.manipulateEntities;
            }
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Called when a tool is installed into a tool manager. Should be called
        /// by the tool manager after the tool is already in the tool stack.
        /// </summary>
        /// <param name="manager">Tool manager that is hosting the tool. Always non-null.</param>
        public override void Install(IToolManager manager)
        {
            base.Install(manager);

            this.SceneSelectionChanged(Scene.Selection, Scene.Selection);
        }

        /// <summary>
        /// Called when a tool is uninstalled from a tool manager. Should be called
        /// by the tool manager after the tool has already been removed form the tool
        /// stack.
        /// </summary>
        public override void Uninstall()
        {
            this.manipulator.ForceAbort();

            base.Uninstall();
        }

        /// <summary>
        /// Draw the tool.
        /// </summary>
        /// <param name="renderData">Rendering data. Always non-null.</param>
        public override void Draw(IRenderData renderData)
        {
            base.Draw(renderData);

            if (Selection.Count == 0)
            {
                return;
            }

            this.DrawManipulator(renderData);
        }

        /// <summary>
        /// Handle input data from a canvas.
        /// </summary>
        /// <param name="sceneInputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        public override InputHandled HandleInput(SceneToolInputData sceneInputData)
        {
            if (Selection.Count != 0)
            {
                InputHandled handled = this.manipulator.HandleInput(sceneInputData);
                if (handled == InputHandled.Yes)
                {
                    return InputHandled.Yes;
                }
            }

            return base.HandleInput(sceneInputData);
        }

        #endregion

        #region Methods - Protected

        /// <summary>
        /// Draw the manipulator tool's manipulator.
        /// </summary>
        /// <param name="renderData">Drawing data.</param>
        protected virtual void DrawManipulator(IRenderData renderData)
        {
            ICanvasRenderer renderer = renderData.Renderer;

            renderer.PushState();
            renderer.AntiAlias = true;

            this.manipulator.Draw(renderData);
            
            renderer.PopState();
        }

        /// <summary>
        /// Called when the tool gets a scene selection changed message.
        /// </summary>
        /// <param name="selection">New selection in the scene.</param>
        /// <param name="oldSelection">Old selection.</param>
        protected override void SceneSelectionChanged(IEntityCollection selection, IEntityCollection oldSelection)
        {
            base.SceneSelectionChanged(selection, oldSelection);

            if (this.Manipulating)
            {
                this.Manipulator.ForceAbort();
            }

            // Register and unregister for changed events.
            foreach (IEntity entity in oldSelection)
            {
                entity.Changed -= this.HandleEntityChanged;
            }

            foreach (IEntity entity in selection)
            {
                entity.Changed += this.HandleEntityChanged;
            }

            // Reset the manipulator before we reposition it.
            this.Manipulator.Reset();

            this.SetupManipulatorRectAndMatrix();
        }

        #endregion

        #region Methods - Protected - Manipulation

        /// <summary>
        /// Setup the manipulator's rect and matrix.
        /// </summary>
        protected virtual void SetupManipulatorRectAndMatrix()
        {
            if (this.Selection == null || this.Selection.Count == 0)
            {
                return;
            }

            Size2D size;
            Matrix2D handleMatrix;

            if (this.Selection.Count == 1)
            {
                IEntity entity = this.Selection[0];

                size = entity.Size;
                handleMatrix = entity.Matrix;
            }
            else
            {
                Rect2D boundingRect = EntityUtility.GetBoundingRect(Selection);
                
                handleMatrix = Matrix2D.Identity;
                handleMatrix.Pos = boundingRect.Pos;

                size = boundingRect.Size;
            }

            this.Manipulator.Matrix = handleMatrix;
            this.Manipulator.Size = size;
        }

        /// <summary>
        /// Handle a manipulation begin.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected virtual void ManipulateBegin(ManipulateEventArgs args)
        {
            if (Selection.Count == 0)
            {
                throw new InvalidOperationException("Selection must not be empty to manipulate.");
            }

            // Build up a set of objects that are select so that we can
            // test to see if a parent is in that set to filter out objects
            // that don't need to be transformed.
            ISet<IEntity> selectionSet = new HashSet<IEntity>();
            foreach (IEntity entity in this.Selection)
            {
                selectionSet.Add(entity);
            }

            this.Manipulating = true;
            this.manipulateEntities.Clear();

            foreach (IEntity entity in this.Selection)
            {
                // If we have a parent and our parent is selected, we won't
                // transform the object since the parent being transformed will
                // cause the child to move. If we transform the child we'll end up
                // double transforming objects.
                if (selectionSet.Contains(entity.Parent))
                {
                    continue;
                }
                
                ManipulatorEntity manipEntity = this.AllocateManipulatorEntity(entity);
                if (manipEntity == null)
                {
                    throw new InvalidOperationException("AllocateManipulatorEntity() should never return null.");
                }

                this.manipulateEntities.Add(manipEntity);
            }
        }

        /// <summary>
        /// Handle a manipulation end.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected virtual void ManipulateEnd(ManipulateEventArgs args)
        {
            this.Manipulating = false;
            this.manipulateEntities.Clear();
        }

        /// <summary>
        /// Handle a manipulation update.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected virtual void ManipulateUpdate(ManipulateEventArgs args)
        {
        }

        /// <summary>
        /// Handle a manipulation abort.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected virtual void ManipulateAbort(ManipulateEventArgs args)
        {
            // Put all entities back where they started.
            foreach (ManipulatorEntity manipEnt in this.Entities)
            {
                IEntity entity = manipEnt.Entity;

                entity.Matrix = manipEnt.StartMatrix;
            }

            this.Manipulating = false;
            this.manipulateEntities.Clear();

            this.SetupManipulatorRectAndMatrix();
        }

        /// <summary>
        /// Allocate a manipulator entity for the passed entity.
        /// </summary>
        /// <param name="entity">Entity that we are going to manipulate.</param>
        /// <returns>Returns a manipulator entity for the passed entity. Must not return null.</returns>
        protected virtual ManipulatorEntity AllocateManipulatorEntity(IEntity entity)
        {
            return new ManipulatorEntity(entity);
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Handle an entity changed event.
        /// </summary>
        /// <param name="args">Changed event arguments.</param>
        private void HandleEntityChanged(EntityChangedEventArgs args)
        {
            if (this.Manipulating)
            {
                return;
            }

            // $TODO - This will get called for every change that happens. That's a lot...

            // Reset the manipulator before we reposition it.
            this.Manipulator.Reset();

            this.SetupManipulatorRectAndMatrix();
        }

        #endregion
    }
}
